package  cursor_animation ;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.Map;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.CaretVisualAttributes;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.view.EditorView;
import com.intellij.util.ReflectionUtil;
import org.jetbrains.annotations.NotNull;

import com.intellij.ide.IdeEventQueue;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.ProjectActivity;
import com.intellij.openapi.util.Disposer;

import com.intellij.openapi.editor.Caret;
import kotlin.Unit;
import kotlin.coroutines.Continuation;
import cursor_animation.EditorPainterProxy;
import cursor_animation.SmoothCaret;

import javax.swing.Timer;

public class customCursorSmoothBlinkAnimation implements ProjectActivity {

    private final Map<Editor, VisualCaretAnimator> caretAnimators = new HashMap<>();

    @Override
    public Object execute(@NotNull Project project, @NotNull Continuation<? super Unit> continuation) {
        for (Editor editor : EditorFactory.getInstance().getAllEditors()) {
            registerAnimator(editor);
        }

        EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryListener() {
            @Override
            public void editorCreated(@NotNull EditorFactoryEvent event) {
                registerAnimator(event.getEditor());
            }

            @Override
            public void editorReleased(@NotNull EditorFactoryEvent event) {
                caretAnimators.remove(event.getEditor());
            }
        }, Disposer.newDisposable("EditorFactory animator listener"));

        IdeEventQueue.getInstance().addDispatcher((awtEvent) -> {
            caretAnimators.values().forEach(VisualCaretAnimator::repaintCarets);
            return false;
        }, Disposer.newDisposable("test"));
        return null;
    }

    private void registerAnimator(Editor editor) {
        if (caretAnimators.containsKey(editor))
            return;
        VisualCaretAnimator caretAnimator = new VisualCaretAnimator(editor);
        caretAnimators.put(editor, caretAnimator);
        editor.getCaretModel().addCaretListener(caretAnimator);
        editor.getDocument().addDocumentListener(caretAnimator);
    }

}
 class VisualCaretAnimator implements CaretListener, DocumentListener {

    private final Map<Caret, SmoothCaret> smoothCarets = new HashMap<>();
    private final Editor editor;

    public VisualCaretAnimator(Editor editor) {
        this.editor = editor;

        ApplicationManager.getApplication().invokeLater(() ->
                editor.getCaretModel().getAllCarets().forEach(this::smoothCaret));
    }

    @Override
    public void caretPositionChanged(@NotNull CaretEvent event) {
        Caret caret = event.getCaret();
        if (caret == null)
            return;
        if(!this.smoothCarets.containsKey(caret))
            this.smoothCaret(caret);
        SmoothCaret smoothCaret = this.smoothCarets.get(caret);
        Point2D to = editor.visualPositionToPoint2D(editor.logicalToVisualPosition(event.getNewPosition()));
        smoothCaret.move(to);
    }

    @Override
    public void documentChanged(@NotNull DocumentEvent event) {
        if (event.getDocument().isInBulkUpdate())
            return;
        for (Caret caret : editor.getCaretModel().getAllCarets()) {
            SmoothCaret smoothCaret = this.smoothCarets.get(caret);
            smoothCaret.move();
        }
    }

    @Override
    public void caretAdded(@NotNull CaretEvent event) {
        Caret caret = event.getCaret();
        if (caret == null)
            return;
        this.smoothCaret(caret);
    }

    @Override
    public void caretRemoved(@NotNull CaretEvent event) {
        Caret caret = event.getCaret();
        if (caret == null)
            return;
        this.smoothCarets.remove(caret);
    }

    private void smoothCaret(Caret caret) {
        this.smoothCarets.put(caret, new SmoothCaret(caret));
        hideCaret(caret);
    }

    private void hideCaret(Caret caret) {
        CaretVisualAttributes attributes = caret.getVisualAttributes();
        ApplicationManager.getApplication().invokeLater(() ->
                caret.setVisualAttributes(new CaretVisualAttributes(attributes.getColor(), attributes.getWeight(), CaretVisualAttributes.Shape.UNDERSCORE, 0)));
    }

    public void repaintCarets() {
        this.smoothCarets.values().forEach(SmoothCaret::repaint);
    }

}
 class SmoothCaret {

    private final Caret caret;
    private final CaretPainter painter;
    private final int duration = 80;
    private Point2D position;
    private Timer caretAnimation;

    public SmoothCaret(Caret caret) {
        this.caret = caret;
        Editor editor = caret.getEditor();
        this.painter = new CaretPainter(this, caret, (EditorImpl) editor, ReflectionUtil.getField(editor.getClass(), editor, EditorView.class, "myView"));

        this.position = editor.visualPositionToPoint2D(caret.getVisualPosition());
        editor.getContentComponent().add(painter);
        editor.getContentComponent().addComponentListener(painter);
    }

    public static Point2D calculateProgress(Point2D startPoint, Point2D targetPoint, double progress) {
        Point2D point = new Point2D.Double();
        if (startPoint != null && targetPoint != null) {
            point.setLocation(
                    calculateProgress(startPoint.getX(), targetPoint.getX(), progress),
                    calculateProgress(startPoint.getY(), targetPoint.getY(), progress)
            );
        }
        return point;
    }

    public static double calculateProgress(double startValue, double endValue, double fraction) {
        double value;
        double distance = endValue - startValue;
        value = distance * fraction;
        value += startValue;
        return value;
    }

    public void move() {
        Editor editor = caret.getEditor();
        move(editor.visualPositionToPoint2D(caret.getVisualPosition()));
    }

    public void move(Point2D endPoint) {
        long startTime = System.currentTimeMillis();
        if (caretAnimation != null)
            caretAnimation.stop();
        caretAnimation = new Timer(10 / 1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                long now = System.currentTimeMillis();
                long diff = now - startTime;
                if (diff < duration) {
                    double progress = (double) diff / (double) duration;
                    position = calculateProgress(position, endPoint, progress);
                } else {
                    ((Timer) e.getSource()).stop();
                }
                repaint();
            }
        });
        caretAnimation.start();
        caretAnimation.setRepeats(true);
        caretAnimation.setCoalesce(true);
        caretAnimation.setInitialDelay(0);
    }

    public void repaint() {
        painter.update();
        painter.repaint();
    }

    public Point2D position() {
        return position;
    }

}